#!/usr/bin/python
#

# This module includes some utilities to bundle a list of MachO binaries and their dependencies.
# It copies the binaries and their dependencies in specified directories and then uses otool
# and install_name_tool to change install names in order to make them relative
# to the specified directories.

import os.path
import subprocess
import sys
import shutil

def execute(cmd):
	shell = subprocess.Popen(cmd, shell=True, bufsize=42000, stdout=subprocess.PIPE)
	(lOutput, lErr) = shell.communicate()
	ret_code = shell.wait()
	if ret_code == 0:
		return lOutput
	else:
		return None

def readDependencies(fileName, keepRawPath = True):
	"""
	Returns a set of the dependencies of the MachO binary given as fileName.
	Ignores dependencies specified in the ignoredPaths.
	Also ignores dependencies whose name starts with @.
	"""

	cmd = "otool -L " + fileName
	output = execute(cmd)
	if not output:
		raise Exception("Problem running otool. [%s]" % (cmd))
	output = output.split("\n")
	deps = set()
	ignoredPaths = ["/System/Library/", "/usr/lib/"]
	ignored = False
	# remove first line, which is the name of th Mach-O binary examined and the last empty element.
	for line in output[1:-1]:
		dylibName = line.split()[0]
		if not keepRawPath:
			dylibName = os.path.normpath(os.path.realpath(dylibName))
		# ignore dependencies which starts with @ because it means it already have been processed.
		ignored = dylibName.startswith('@')
		if not ignored:
			if os.path.exists(dylibName):
				for ignPath in ignoredPaths:
					if dylibName.startswith(ignPath):
						ignored = True
						break
				if not ignored:
					deps.add(dylibName)
			else:
				print "Warning : ", fileName, "depends on", dylibName, "but this file does not exists. We skipped it in dependency collection."
	return deps

def collectDependencies(fileName, pDependencies):
	"""
	Recursively collect dependencies in the result parameter pDependencies.
	"""
	firstLevelDepList = readDependencies(fileName)
	firstLevelDepList -= pDependencies
	pDependencies |= firstLevelDepList
	for dep in firstLevelDepList:
		try:
			collectDependencies(dep, pDependencies)
		except:
			if not os.path.basename(dep) == os.path.basename(fileName):
				raise
			else:
				print "Warning,", fileName, "depends on", dep, "but we skipped this dependency because it has the same basename."

def copyDeps(pDependencies, destDir):
	for d in pDependencies:
		shutil.copy(d, destDir)

def makeInstallNamesRelative(pMachO, pMachODirectory, pMainPrefix, pDepsPrefix):
	lMOLocalPath = os.path.join(pMachODirectory, os.path.basename(pMachO))
	cmd = "install_name_tool -id " + os.path.join(pMainPrefix, os.path.basename(pMachO)) + " " + lMOLocalPath
	execute(cmd)
	lMODeps = readDependencies(lMOLocalPath, True)
	for dep in lMODeps:
		cmd = "install_name_tool -change " + dep + " " + os.path.join(pDepsPrefix, os.path.basename(dep)) + " " + lMOLocalPath
		execute(cmd)

def package(pMachOList, pMachODir, pDepsDir, pMachOPrefix, pDepsPrefix):
	deps = set()
	for machO in pMachOList:
		print "Collecting", machO, "dependencies..."
		collectDependencies(machO, deps)
		print "Done."
	print "Found", len(deps), "distinct dependencies."
	for machO in pMachOList:
		print "Copying", machO
		shutil.copy(machO, pMachODir)
	print "Copying dependencies..."
	copyDeps(deps, pDepsDir)
	print "Done."

	print "Making install names relative..."
	for machO in pMachOList:
		makeInstallNamesRelative(machO, pMachODir, pMachOPrefix, pDepsPrefix)
	for dep in deps:
		makeInstallNamesRelative(dep, pDepsDir, pDepsPrefix, pDepsPrefix)
	print "Done."

def main(argv) :
	# usage example
	os.mkdir("./sample.app")
	os.mkdir("./sample.app/Contents/")
	os.mkdir("./sample.app/Contents/Frameworks")
	os.mkdir("./sample.app/Contents/MacOS")
	mainMachO = argv[1]
	mainMachODir = "./sample.app/Contents/MacOS/"
	libsDestinationDirectory = "./sample.app/Contents/Frameworks/"
	package([mainMachO], mainMachODir, libsDestinationDirectory,  "@loader_path/../MacOS/", "@loader_path/../Frameworks/")

if __name__ == "__main__":
	main(sys.argv)

